package tink.macro;

#if macro
	import haxe.macro.Context;
	import haxe.macro.Expr;
	/**
	 * This class provides functionality for expression mangling.
	 * All methods are instance methods, because they thus can be comfortably 'imported' by subclassing.
	 */
	class Helper {		
		/**
		 * logs an expression and returns it
		 * @param	e
		 * @return
		 */
		public inline function logExpr(e:Expr):Expr {
			trace(printExpr(e));
			return e;
		}
		/**
		 * Creates an enum. Please note, that the constructors are not prefixed by the enum, and the enum must thus be imported
		 * @param	constr
		 * @param	?args
		 * @param	?pos
		 */
		public function makeEnum(constr:String, ?args:Array<Expr>, ?pos) {
			return 
				if (args == null || args.length == null) 
					ident(constr);
				else 
					expr(ECall(ident(constr, pos), args), pos);
		}
		/**
		 * Transforms an expression tree into readable code. Indenting might be added in the future
		 * @param	e
		 * @return
		 */
		public function printExpr(e:Expr):String {
			return ExprPrinter.printExpr('', e);
		}
		public function error(s:String, ?pos:Position):Dynamic {
			if (pos == null) pos = Context.currentPos();
			Context.error(s, pos);
		}
		/**
		 * Builds a block. If statements are ommitted, the block is empty.
		 * @param	?statements
		 * @param	?pos
		 */
		public function cBlock(?statements, ?pos:Position) {
			if (statements == null) statements = [];
			return expr(EBlock(statements), pos);
		}
		/**
		 * Builds an if statement.
		 * @param	test
		 * @param	body
		 * @param	alternative
		 * @param	?pos
		 */
		public function cIf(test, body, alternative, ?pos:Position) {
			return expr(EIf(test, body, alternative), pos);
		}
		/**
		 * Builds a single variable declaration.
		 * @param	name
		 * @param	?init
		 * @param	?typ
		 * @param	?pos
		 */
		public function cVar(name:String, ?init:Expr, ?typ:ComplexType, ?pos:Position) {
			return expr(EVars([ { name:name, type: typ, expr: init } ]), pos);
		}
		/**
		 * Builds an expression.
		 * @param	e
		 * @param	?pos:Position
		 */
		public function expr(e:ExprDef, ?pos:Position) {
			if (pos == null) pos = Context.currentPos();
			return {
				expr:e,
				pos:pos
			}
		}
		/**
		 * Builds an type value expression based on a '.'-separated path.
		 * @param	s
		 * @param	?pos:Position
		 */
		public function typ(s:String, ?pos:Position):Expr {
			var path = s.split('.');
			return
				if (path.length == 1) 
					expr(EConst(CType(s)), pos);
				else {
					var type = path.pop();
					var root = path.shift();
					expr(EType(aPath(ident(root), path), type), pos);
				}
		}
		/**
		 * builds a global call expression
		 * @param	s
		 * @param	args
		 * @param	?pos:Position
		 */
		public function call(s:String, args:Array<Expr>, ?pos:Position):Expr {
			var parts = s.split('.');
			var func = parts.pop();
			s = parts.join('.');
			return expr(ECall(expr(EField(typ(s), func), pos), args), pos);
		}
		/**
		 * builds an identifier expression
		 * @param	s
		 * @param	?pos:Position
		 */
		public function ident(s:String, ?pos:Position):Expr {
			return expr(EConst(CIdent(s)), pos);
		}
		/**
		 * builds a string constant expression
		 * @param	s
		 * @param	?pos
		 * @return
		 */
		public function string(s:String, ?pos:Position):Expr {
			return expr(EConst(CString(s)), pos);
		}
		/**
		 * Builds an expression, that traverses a field path starting from a given target expression, the path being an array
		 * @param	target
		 * @param	path
		 */
		public function aPath(target:Expr, path:Array<String>, ?pos:Position):Expr {
			for (part in path) 
				target = expr(EField(target, part), pos);
			return target;			
		}
		/**
		 * Builds an expression, that traverses a field path starting from a given target expression, the path being a '.'-separated string.
		 * @param	target
		 * @param	path
		 */
		public function sPath(target:Expr, path:String, ?pos:Position):Expr {
			return 
				if (path.length > 0) aPath(target, path.split('.'), pos);
				else target;
		}
		/**
		 * Attempts to extract a string from an expression. This may become obsolete in haxe 2.08
		 * @param	e
		 */
		public function getString(string:Expr):String {
			return
				switch (string.expr) {
					case EConst(c):
						switch (c) {
							case CString(s): s;
							default: error("string expected", string.pos);
						}
					default: error("string expected", string.pos);
				}					
		}
		/**
		 * Attempts to extract an identifier from an expression.
		 * @param	e
		 */
		public function getIdent(ident:Expr):String {
			return
				switch (ident.expr) {
					case EConst(c):
						switch (c) {
							case CIdent(ident): 
								ident;
							default:
								error('expected identifier', ident.pos);
						}
					default:
						error('expected identifier', ident.pos);						
				}			
		}
	}
	private class ExprPrinter {
		static var binops = '+,*,/,-,=,==,!=,>,>=,<,<=,&,|,^,&&,||,<<,>>,>>>,%,NONE,...'.split(',');
		static function binop(b:Binop) {
			return
				switch (b) {
					case OpAssignOp(op):
						binop(op) + '=';
					default:
						binops[Type.enumIndex(b)];
				}
		}
		static function printExprList(indent:String, list:Iterable<Expr>, ?sep = ', ', ?border:Array<String>) {
			if (border == null) border = '()'.split('');
			return border[0] + Lambda.map(list, callback(printExpr,indent)).join(sep) + border[1];
		}
		static function printPath(p:TypePath) {
			var a = p.pack.copy();
			a.push(p.name);
			if (p.sub != null) a.push(p.sub);
			return a.join('.');//omitting type parameters here
		}
		static function printType(t:ComplexType) {
			//this is really a bit rudimental :D
			return
				switch (t) {
					case TPath(p): 
						printPath(p);
					case TFunction(args, ret):
						'Dynamic';
					case TAnonymous(_): 
						'Dynamic';
					case TParent(t): 
						printType(t);
					case TExtend(p,_): 
						printPath(p);
				}
		}
		static var unops = '++,--,!,-,~'.split(',');
		@:macro static function rec(e:Expr) {
			return Magic.AST(printExpr(indent, eval(e)));
		}
		static public function printExpr(indent:String, e:Expr):String {
			return 
				switch (e.expr) {
					case EConst(c):
						switch (c) {
							case CInt(s), CFloat(s), CIdent(s), CType(s): s;
							case CString(s): '"' + s + '"';
							case CRegexp(r, opt): '~/' + r + '/' + opt;
						}
					case EVars(vars):
						var ret = [];
						for (v in vars) {
							ret.push(v.name
								+ (if (v.type == null) '' else ' : ' + printType(v.type))
								+ (if (v.expr == null) '' else ' = ' + rec(v.expr))
							);
						}
						'var ' + ret.join(', ');
					case ECast(e, t):
						'cast(' + rec(e) + ((t == null) ? '' : ', ' + printType(t)) + ')';
					case EArray(e1, e2): 
						rec(e1) + '[' + rec(e2) + ']';
					case EField(e, field), EType(e, field):
						rec(e) + '.' + field;
					case EParenthesis(e):
						'(' + rec(e) + ')';
					case ECall(e, params):
						rec(e) + printExprList(indent, params);
					case EIf(econd, eif, eelse):
						'if (' + rec(econd) + ') ' + rec(eif) + 
							if (eelse == null) 
								'' 
							else 
								('\n' + indent + 'else ' + rec(eelse));
					case EBlock(exprs):
						if (exprs.length == 0) 
							'{}';
						else
							printExprList(indent + '\t', exprs, ';\n\t' + indent, ['{\n\t' + indent, ';\n' + indent + '}']);
					case EFor(v, it, expr):
						'for (' + v + ' in ' + rec(it) + ')\n' + rec(expr);
					case EWhile(econd, e, normalWhile):
						if (normalWhile)
							'while (' + rec(econd) + ') ' + rec(e);
						else 
							'do ' + rec(e) + '\nwhile (' + rec(econd) + ')';
					case EBreak: 'break';
					case EContinue: 'continue';
					case EUntyped(e): 'untyped ' + rec(e);
					case EThrow(e): 'throw ' + rec(e);
					case EReturn(e): 'return' + if (e == null) '' else (' ' + rec(e));
					case EDisplay(e, isCall):
						throw 'don\'t know what EDisplay is';//feel free to implement
					case EDisplayNew(t):
						throw 'don\'t know what EDisplayNew is';//feel free to implement
					case ETernary(econd, eif, eelse):
						return '((' + rec(econd) + ') ? ' + rec(eif) + ' : ' + rec(eelse) + ')';
					case ESwitch(e, cases, edef):
						var ret = [];
						for (c in cases)
							ret.push('case ' + printExprList(indent, c.values, ['','']) + ': ' + printExpr(indent + '\t', c.expr));
						if (edef != null) 
							ret.push('default: ' + rec(edef));
						'switch (' + rec(e) + ') {\n\t' + indent + ret.join('\n\t' + indent) + '\n' + indent + '}';
					case EUnop(op, postFix, e):
						var op = unops[Type.enumIndex(op)];
						var e = rec(e);
						var inner = 
							if (postFix) 
								e + op;
							else
								op + e;
						'(' + inner + ')';
					case ETry(e, catches):
						var ret = [];
						for (c in catches)
							ret.push('catch (' + c.name +' : ' + printType(c.type) + ') ' + rec(c.expr));
						'try ' + rec(e) + '\n' + indent + ret.join('\n' + indent);
					case ENew(t, params):
						'new ' + printPath(t) + printExprList(indent, params);
					case EBinop(op, e1, e2):
						'(' + rec(e1) + binop(op) + rec(e2) + ')';
					case EArrayDecl(values):
						printExprList(indent, values,'[]'.split(''));
					default: 
						/**
						 * TODO: implement to following:
						 * - EFunction
						 * - EObjectDecl
						 */
						throw 'not implemented: ' + Type.enumConstructor(e.expr);
				}
		}		
	}
#end